package jxtend.processor.eclipse.visitor;

import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACC_SUPER;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.GETFIELD;
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.IRETURN;
import static org.objectweb.asm.Opcodes.PUTFIELD;
import static org.objectweb.asm.Opcodes.RETURN;
import static org.objectweb.asm.Opcodes.V1_6;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import jxtend.processor.eclipse.utils.ASTNodeUtils;
import jxtend.processor.eclipse.visitor.handler.ASTNodeVisitorContext;

import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;

@SuppressWarnings("restriction")
/* default */ final class ASTVisitorFactory {

	public static ASTVisitor create(ASTNodeVisitor astNodeVisitor, List<ASTNode> parentNodes) {
		try {
			return astVisitorConstructor.newInstance(astNodeVisitor, new ContextImpl(parentNodes));
		} catch (IllegalArgumentException e) {
			throw new RuntimeException(e);
		} catch (InstantiationException e) {
			throw new RuntimeException(e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(e);
		}
	}

	private static final String astVisitorClassName =
			ASTVisitorFactory.class.getName() + "$ASTVisitorImpl";
	private static final String astVisitorClassNameWithSlashes =
			ASTVisitorFactory.class.getName().replace('.', '/') + "$ASTVisitorImpl";

	private static final ExtendedClassLoader extendedClassLoader =
			new ExtendedClassLoader(ASTVisitorFactory.class.getClassLoader());

	private static final Class<ASTVisitor> astVisitorImplType;
	private static final Constructor<ASTVisitor> astVisitorConstructor;
	
	static {
		long start = System.currentTimeMillis();

		ClassWriter classWriter = createClassWriter();
		createConstructor(classWriter);
		Method[] methods = ASTVisitor.class.getMethods();
		for (Method method : methods) {
			if (method.getName().equals("visit")) {
				@SuppressWarnings({ "unchecked", "rawtypes" })
				Class<? extends ASTNode> astNodeType = (Class) method.getParameterTypes()[0];
				@SuppressWarnings({ "unchecked", "rawtypes" })
				Class<? extends Scope> scopeType = (Class) method.getParameterTypes()[1];

				createVisitMethod(classWriter, astNodeType, scopeType);
			} else if (method.getName().equals("endVisit")) {
				@SuppressWarnings({ "unchecked", "rawtypes" })
				Class<? extends ASTNode> astNodeType = (Class) method.getParameterTypes()[0];
				@SuppressWarnings({ "unchecked", "rawtypes" })
				Class<? extends Scope> scopeType = (Class) method.getParameterTypes()[1];

				createEndVisitMethod(classWriter, astNodeType, scopeType);
			}
		}
		
		classWriter.visitEnd();

		astVisitorImplType = (Class) extendedClassLoader.defineClass(astVisitorClassName, classWriter.toByteArray());
		try {
			astVisitorConstructor =
					astVisitorImplType.getConstructor(ASTNodeVisitor.class, ASTNodeVisitorContext.class);
		} catch (SecurityException e) {
			throw new RuntimeException(e);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(e);
		}

//		System.out.println("&&& Seconds [CREATE AST VISITOR]: " + (System.currentTimeMillis() - start) / 1000D);
	}

	public static ClassWriter createClassWriter() {
		ClassWriter result = new ClassWriter(0);
		result.visit(V1_6, ACC_PUBLIC + ACC_SUPER + ACC_STATIC, astVisitorClassNameWithSlashes, null,
				"org/eclipse/jdt/internal/compiler/ASTVisitor", null);

		FieldVisitor fieldVisitor;

		fieldVisitor = result.visitField(
				ACC_PRIVATE, "astNodeVisitor", "Ljxtend/processor/eclipse/visitor/ASTNodeVisitor;", null, null);
		fieldVisitor.visitEnd();

		fieldVisitor = result.visitField(
				ACC_PRIVATE, "context", "Ljxtend/processor/eclipse/visitor/handler/ASTNodeVisitorContext;", null, null);
		fieldVisitor.visitEnd();

		return result;
	}

	private static MethodVisitor createConstructor(ClassWriter classWriter) {
		MethodVisitor result = classWriter.visitMethod(ACC_PUBLIC, "<init>",
				"(Ljxtend/processor/eclipse/visitor/ASTNodeVisitor;"
				+ "Ljxtend/processor/eclipse/visitor/handler/ASTNodeVisitorContext;)V", null, null);
		result.visitCode();

		Label l0 = new Label();
		result.visitLabel(l0);
		result.visitVarInsn(ALOAD, 0);
		result.visitMethodInsn(INVOKESPECIAL, "org/eclipse/jdt/internal/compiler/ASTVisitor", "<init>", "()V");
		Label l1 = new Label();
		result.visitLabel(l1);
		result.visitVarInsn(ALOAD, 0);
		result.visitVarInsn(ALOAD, 1);
		result.visitFieldInsn(PUTFIELD, astVisitorClassNameWithSlashes, "astNodeVisitor", "Ljxtend/processor/eclipse/visitor/ASTNodeVisitor;");
		Label l2 = new Label();
		result.visitLabel(l2);
		result.visitVarInsn(ALOAD, 0);
		result.visitVarInsn(ALOAD, 2);
		result.visitFieldInsn(PUTFIELD, astVisitorClassNameWithSlashes, "context", "Ljxtend/processor/eclipse/visitor/handler/ASTNodeVisitorContext;");
		Label l3 = new Label();
		result.visitLabel(l3);
		result.visitInsn(RETURN);
		Label l4 = new Label();
		result.visitLabel(l4);
		result.visitLocalVariable("this", "L" + astVisitorClassNameWithSlashes + ";", null, l0, l4, 0);
		result.visitLocalVariable("astNodeVisitor", "Ljxtend/processor/eclipse/visitor/ASTNodeVisitor;", null, l0, l4, 1);
		result.visitLocalVariable("context", "Ljxtend/processor/eclipse/visitor/handler/ASTNodeVisitorContext;", null, l0, l4, 2);
		result.visitMaxs(2, 3);
		result.visitEnd();

		return result;
	}

	private static MethodVisitor createVisitMethod(
			ClassWriter classWriter, Class<? extends ASTNode> astNodeType, Class<? extends Scope> scopeType) {
		MethodVisitor result = classWriter.visitMethod(ACC_PUBLIC, "visit",
				"(L" + astNodeType.getName().replace('.', '/') + ";L" + scopeType.getName().replace('.', '/') + ";)Z",
				null, null);
		result.visitCode();
		Label l0 = new Label();
		result.visitLabel(l0);
		result.visitVarInsn(ALOAD, 0);
		result.visitFieldInsn(GETFIELD, astVisitorClassNameWithSlashes, "astNodeVisitor",
				"Ljxtend/processor/eclipse/visitor/ASTNodeVisitor;");
		result.visitVarInsn(ALOAD, 0);
		result.visitFieldInsn(GETFIELD, astVisitorClassNameWithSlashes, "context",
				"Ljxtend/processor/eclipse/visitor/handler/ASTNodeVisitorContext;");
		result.visitVarInsn(ALOAD, 1);
		result.visitVarInsn(ALOAD, 2);
		result.visitMethodInsn(INVOKEINTERFACE, "jxtend/processor/eclipse/visitor/ASTNodeVisitor", "visit",
				"(Ljxtend/processor/eclipse/visitor/handler/ASTNodeVisitorContext;"
					+ "Lorg/eclipse/jdt/internal/compiler/ast/ASTNode;"
					+ "Lorg/eclipse/jdt/internal/compiler/lookup/Scope;)Z");
		result.visitInsn(IRETURN);
		Label l1 = new Label();
		result.visitLabel(l1);
		result.visitLocalVariable("this", "L" + astVisitorClassNameWithSlashes + ";", null, l0, l1, 0);
		result.visitLocalVariable("node", "L"+ astNodeType.getName().replace('.', '/') +";", null, l0, l1, 1);
		result.visitLocalVariable("scope", "L" + scopeType.getName().replace('.', '/') + ";", null, l0, l1, 2);
		result.visitMaxs(4, 3);
		result.visitEnd();
		result.visitEnd();

		return result;
	}

	private static MethodVisitor createEndVisitMethod(
			ClassWriter classWriter, Class<? extends ASTNode> astNodeType, Class<? extends Scope> scopeType) {
		MethodVisitor result = classWriter.visitMethod(ACC_PUBLIC, "endVisit",
				"(L" + astNodeType.getName().replace('.', '/') + ";L" + scopeType.getName().replace('.', '/') + ";)V",
				null, null);
		result.visitCode();
		Label l0 = new Label();
		result.visitLabel(l0);
		result.visitVarInsn(ALOAD, 0);
		result.visitFieldInsn(GETFIELD, astVisitorClassNameWithSlashes, "astNodeVisitor",
				"Ljxtend/processor/eclipse/visitor/ASTNodeVisitor;");
		result.visitVarInsn(ALOAD, 0);
		result.visitFieldInsn(GETFIELD, astVisitorClassNameWithSlashes, "context",
				"Ljxtend/processor/eclipse/visitor/handler/ASTNodeVisitorContext;");
		result.visitVarInsn(ALOAD, 1);
		result.visitVarInsn(ALOAD, 2);
		result.visitMethodInsn(INVOKEINTERFACE, "jxtend/processor/eclipse/visitor/ASTNodeVisitor", "endVisit",
				"(Ljxtend/processor/eclipse/visitor/handler/ASTNodeVisitorContext;"
						+ "Lorg/eclipse/jdt/internal/compiler/ast/ASTNode;"
						+ "Lorg/eclipse/jdt/internal/compiler/lookup/Scope;)V");
		Label l1 = new Label();
		result.visitLabel(l1);
		result.visitInsn(RETURN);
		Label l2 = new Label();
		result.visitLabel(l2);
		result.visitLocalVariable("this", "L" + astVisitorClassNameWithSlashes + ";", null, l0, l2, 0);
		result.visitLocalVariable("node", "L"+ astNodeType.getName().replace('.', '/') +";", null, l0, l2, 1);
		result.visitLocalVariable("scope", "L" + scopeType.getName().replace('.', '/') + ";", null, l0, l2, 2);
		result.visitMaxs(4, 3);
		result.visitEnd();

		return result;
	}

	private static class ExtendedClassLoader extends ClassLoader {
		public ExtendedClassLoader(ClassLoader parent) {
			super(parent);
		}

		public Class<?> defineClass(String name, byte[] b) {
			return defineClass(name, b, 0, b.length);
		}	
	}

	private static final class ContextImpl implements ASTNodeVisitorContext {
		private final List<ASTNode> parentNodes = new ArrayList<ASTNode>();
		private final List<ASTNode> callerParentNodes;

		public ContextImpl(List<ASTNode> callerParentNodes) {
			this.callerParentNodes = callerParentNodes;
		}

		@Override
		public ASTNode getParentNode() {
			return !parentNodes.isEmpty() ? parentNodes.get(parentNodes.size() - 1)
					: (!callerParentNodes.isEmpty() ? callerParentNodes.get(callerParentNodes.size() - 1) : null);
		}

		@Override
		public ASTNode getParentNode(int index) {
			return index < parentNodes.size() ? parentNodes.get(parentNodes.size() - index - 1)
					: callerParentNodes.get(callerParentNodes.size() - index - parentNodes.size() - 1);
		}

		@Override
		public <A extends ASTNode> A findParentNode(Class<A> parentNodeType) {
			for (int i = parentNodes.size() - 1; i >= 0; i--) {
				ASTNode currentNode = parentNodes.get(i);
				
				if (parentNodeType.isAssignableFrom(ASTNodeUtils.getClassForNormalKindASTNode(currentNode))) {
					@SuppressWarnings("unchecked") A result = (A) currentNode; 
					return result;
				}
			}

			for (int i = callerParentNodes.size() - 1; i >= 0; i--) {
				ASTNode currentNode = callerParentNodes.get(i);
				
				if (parentNodeType.isAssignableFrom(ASTNodeUtils.getClassForNormalKindASTNode(currentNode))) {
					@SuppressWarnings("unchecked") A result = (A) currentNode; 
					return result;
				}
			}

			return null;
		}
	}
}
